import { compileExpr, compileExprToArray, QWebVar } from "./expression_parser";

export const INTERP_REGEXP = /\{\{.*?\}\}/g;
//------------------------------------------------------------------------------
// Compilation Context
//------------------------------------------------------------------------------

export class CompilationContext {
  static nextID: number = 1;
  code: string[] = [];
  variables: { [key: string]: QWebVar } = {};
  escaping: boolean = false;
  parentNode: number | null | string = null;
  parentTextNode: number | null = null;
  rootNode: number | null = null;
  indentLevel: number = 0;
  rootContext: CompilationContext;
  shouldDefineParent: boolean = false;
  shouldDefineScope: boolean = false;
  protectedScopeNumber: number = 0;
  shouldDefineQWeb: boolean = false;
  shouldDefineUtils: boolean = false;
  shouldDefineRefs: boolean = false;
  shouldDefineResult: boolean = true;
  loopNumber: number = 0;
  inPreTag: boolean = false;
  templateName: string;
  allowMultipleRoots: boolean = false;
  hasParentWidget: boolean = false;
  hasKey0: boolean = false;
  keyStack: boolean[] = [];

  constructor(name?: string) {
    this.rootContext = this;
    this.templateName = name || "noname";
    this.addLine("let h = this.h;");
  }

  generateID(): number {
    return CompilationContext.nextID++;
  }

  /**
   * This method generates a "template key", which is basically a unique key
   * which depends on the currently set keys, and on the iteration numbers (if
   * we are in a loop).
   *
   * Such a key is necessary when we need to associate an id to some element
   * generated by a template (for example, a component)
   */
  generateTemplateKey(prefix: string = ""): string {
    const id = this.generateID();
    if (this.loopNumber === 0 && !this.hasKey0) {
      return `'${prefix}__${id}__'`;
    }
    let key = `\`${prefix}__${id}__`;
    let start = this.hasKey0 ? 0 : 1;
    for (let i = start; i < this.loopNumber + 1; i++) {
      key += `\${key${i}}__`;
    }
    this.addLine(`let k${id} = ${key}\`;`);
    return `k${id}`;
  }

  generateCode(): string[] {
    if (this.shouldDefineResult) {
      this.code.unshift("    let result;");
    }

    if (this.shouldDefineScope) {
      this.code.unshift("    let scope = Object.create(context);");
    }
    if (this.shouldDefineRefs) {
      this.code.unshift("    context.__owl__.refs = context.__owl__.refs || {};");
    }
    if (this.shouldDefineParent) {
      if (this.hasParentWidget) {
        this.code.unshift("    let parent = extra.parent;");
      } else {
        this.code.unshift("    let parent = context;");
      }
    }
    if (this.shouldDefineQWeb) {
      this.code.unshift("    let QWeb = this.constructor;");
    }
    if (this.shouldDefineUtils) {
      this.code.unshift("    let utils = this.constructor.utils;");
    }
    return this.code;
  }

  withParent(node: number): CompilationContext {
    if (
      !this.allowMultipleRoots &&
      this === this.rootContext &&
      (this.parentNode || this.parentTextNode)
    ) {
      throw new Error("A template should not have more than one root node");
    }
    if (!this.rootContext.rootNode) {
      this.rootContext.rootNode = node;
    }
    if (!this.parentNode && this.rootContext.shouldDefineResult) {
      this.addLine(`result = vn${node};`);
    }
    return this.subContext("parentNode", node);
  }

  subContext(key: keyof CompilationContext, value: any): CompilationContext {
    const newContext = Object.create(this);
    newContext[key] = value;
    return newContext;
  }

  indent() {
    this.rootContext.indentLevel++;
  }

  dedent() {
    this.rootContext.indentLevel--;
  }

  addLine(line: string): number {
    const prefix = new Array(this.indentLevel + 2).join("    ");
    this.code.push(prefix + line);
    return this.code.length - 1;
  }

  addIf(condition: string) {
    this.addLine(`if (${condition}) {`);
    this.indent();
  }

  addElse() {
    this.dedent();
    this.addLine("} else {");
    this.indent();
  }

  closeIf() {
    this.dedent();
    this.addLine("}");
  }

  getValue(val: any): QWebVar | string {
    return val in this.variables ? this.getValue(this.variables[val]) : val;
  }

  /**
   * Prepare an expression for being consumed at render time.  Its main job
   * is to
   * - replace unknown variables by a lookup in the context
   * - replace already defined variables by their internal name
   */
  formatExpression(expr: string): string {
    this.rootContext.shouldDefineScope = true;
    return compileExpr(expr, this.variables);
  }
  captureExpression(expr: string): string {
    this.rootContext.shouldDefineScope = true;
    const argId = this.generateID();
    const tokens = compileExprToArray(expr, this.variables);
    const done = new Set();
    return tokens
      .map((tok, i) => {
        // "this" in captured expressions should be the current component
        if (tok.value === "this") {
          if (!done.has("this")) {
            done.add("this");
            this.addLine(`const this_${argId} = utils.getComponent(context);`);
          }
          tok.value = `this_${argId}`;
        }
        // Variables that should be looked up in the scope. isLocal is for arrow
        // function arguments that should stay untouched (eg "ev => ev" should
        // not become "const ev_1 = scope['ev']; ev_1 => ev_1")
        if (
          tok.varName &&
          !tok.isLocal &&
          // HACK: for backwards compatibility, we don't capture bare methods
          // this allows them to be called with the rendering context/scope
          // as their this value.
          (!tokens[i + 1] || tokens[i + 1].type !== "LEFT_PAREN")
        ) {
          if (!done.has(tok.varName)) {
            done.add(tok.varName);
            this.addLine(`const ${tok.varName}_${argId} = ${tok.value};`);
          }
          tok.value = `${tok.varName}_${argId}`;
        }
        return tok.value;
      })
      .join("");
  }

  /**
   * Perform string interpolation on the given string. Note that if the whole
   * string is an expression, it simply returns it (formatted and enclosed in
   * parentheses).
   * For instance:
   *   'Hello {{x}}!' -> `Hello ${x}`
   *   '{{x ? 'a': 'b'}}' -> (x ? 'a' : 'b')
   */
  interpolate(s: string): string {
    let matches = s.match(INTERP_REGEXP);
    if (matches && matches[0].length === s.length) {
      return `(${this.formatExpression(s.slice(2, -2))})`;
    }

    let r = s.replace(/\{\{.*?\}\}/g, (s) => "${" + this.formatExpression(s.slice(2, -2)) + "}");
    return "`" + r + "`";
  }
  startProtectScope(codeBlock?: boolean): number {
    const protectID = this.generateID();
    this.rootContext.protectedScopeNumber++;
    this.rootContext.shouldDefineScope = true;
    const scopeExpr = `Object.create(scope);`;
    this.addLine(`let _origScope${protectID} = scope;`);
    this.addLine(`scope = ${scopeExpr}`);
    if (!codeBlock) {
      this.addLine(`scope.__access_mode__ = 'ro';`);
    }
    return protectID;
  }
  stopProtectScope(protectID: number) {
    this.rootContext.protectedScopeNumber--;
    this.addLine(`scope = _origScope${protectID};`);
  }
}
